package com.cskaoyan.mall.order.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cskaoyan.mall.common.result.Result;
import com.cskaoyan.mall.common.util.DateUtil;
import com.cskaoyan.mall.order.client.PayApiClient;
import com.cskaoyan.mall.order.client.WareApiClient;
import com.cskaoyan.mall.order.constant.OrderStatus;
import com.cskaoyan.mall.order.constant.OrderType;
import com.cskaoyan.mall.order.converter.OrderDetailConverter;
import com.cskaoyan.mall.order.converter.OrderInfoConverter;
import com.cskaoyan.mall.order.dto.OrderDetailDTO;
import com.cskaoyan.mall.order.dto.OrderInfoDTO;
import com.cskaoyan.mall.order.mapper.OrderDetailMapper;
import com.cskaoyan.mall.order.mapper.OrderInfoMapper;
import com.cskaoyan.mall.order.model.OrderDetail;
import com.cskaoyan.mall.order.model.OrderInfo;
import com.cskaoyan.mall.order.query.OrderInfoParam;
import com.cskaoyan.mall.order.service.OrderService;
import com.cskaoyan.mall.pay.api.dto.PaymentInfoDTO;
import com.cskaoyan.mall.ware.api.dto.WareOrderTaskDTO;
import com.cskaoyan.mall.ware.api.dto.WareSkuDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.*;

/**
 * @Author Pudding
 * @Date 2024/7/17 11:44
 */
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    OrderInfoMapper orderInfoMapper;
    @Autowired
    OrderDetailMapper orderDetailMapper;
    @Autowired
    OrderInfoConverter orderInfoConverter;
    @Autowired
    WareApiClient wareApiClient;
    @Autowired
    OrderDetailConverter orderDetailConverter;
    @Autowired
    PayApiClient payApiClient;


    /**
     * 1. 提交订单: 校验价格
     */
    @Override
    public Boolean checkPrice(Long skuId, BigDecimal skuPrice) {
        return null;
    }

    /**
     * 2. 提交订单: 更新用户购物车中商品价格
     */
    @Override
    public void refreshPrice(Long skuId, String userId) {

    }

    /**
     * 3. 保存订单和订单明细
     * @param orderInfo
     * @return 返回orderId
     */
    @Override
    //@Transactional(rollbackFor = RuntimeException.class)
    public Long saveOrderInfo(OrderInfo orderInfo) {

        //1. 保存订单
        orderInfoMapper.insert(orderInfo);

        //2. 保存订单明细
        for (OrderDetail orderDetail : orderInfo.getOrderDetailList()) {
            orderDetail.setId(null);
            orderDetail.setOrderId(orderInfo.getId());

            orderDetailMapper.insert(orderDetail);
        }

        return orderInfo.getId();
    }


    /**
     * 4. 提交订单: 根据订单id获取订单信息
     */
    @Override
    public OrderInfoDTO getOrderInfo(Long orderId) {

        //1. 根据orderId查询订单记录
        OrderInfo orderInfo = orderInfoMapper.selectOne(new LambdaQueryWrapper<OrderInfo>().eq(OrderInfo::getId, orderId));
        //2. 根据订单id查询订单明细
        List<OrderDetail> orderDetailList = orderDetailMapper.selectList(new LambdaQueryWrapper<OrderDetail>().eq(OrderDetail::getOrderId, orderId));
        //3. 把订单明细设置到订单对象中
        orderInfo.setOrderDetailList(orderDetailList);
        //4. 将订单PO对象转换为DTO对象
        OrderInfoDTO orderInfoDTO = orderInfoConverter.convertOrderInfoToOrderInfoDTO(orderInfo);
        //5. 给OrderInfoDTO中的属性orderStatusName赋值
        orderInfoDTO.setOrderStatusName(OrderStatus.getStatusDescByStatus(orderInfoDTO.getOrderStatus()));

        return orderInfoDTO;
    }

    /**
     * 5. 分页获取我的订单列表
     */
    @Override
    public IPage<OrderInfoDTO> getPage(Page<OrderInfo> pageParam, String userId) {

        //2. 分页查询这个用户的订单
        // 分页参数已知、不仅仅要查询订单，还要查询订单明细
        // 第一种思路:
            // 先分页查订单，然后循环订单列表，查每一个订单的详细信息
        LambdaQueryWrapper<OrderInfo> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(OrderInfo::getUserId, userId)
                          .orderByDesc(OrderInfo::getCreateTime);
        Page<OrderInfo> orderInfoPage = orderInfoMapper.selectPage(pageParam, lambdaQueryWrapper);
        //获取当前页的记录
        List<OrderInfo> orderInfoList = orderInfoPage.getRecords();

        orderInfoList.forEach(orderInfo -> {
            //3. 获取订单id
            Long orderId = orderInfo.getId();
            //4. 根据订单id查询订单明细
            List<OrderDetail> orderDetailList = orderDetailMapper.selectList(new LambdaQueryWrapper<OrderDetail>().eq(OrderDetail::getOrderId, orderId).orderByDesc(OrderDetail::getCreateTime));
            //5. 把订单明细设置到订单对象中
            orderInfo.setOrderDetailList(orderDetailList);
        });

        //将Page<OrderInfo>转换为Page<OrderInfoDTO>
        Page<OrderInfoDTO> orderInfoDTOPage = orderInfoConverter.convertPage(orderInfoPage);

        return orderInfoDTOPage;

        // 第二种思路:
        // 直接写连接查询的SQL语句，查询订单信息和订单明细
        // 然后在mapper接口的方法中，添加分页参数
        // 把方法的返回值类型，设置为Page类型
    }

    /**
     * 6. 支付回调，支付成功，修改订单状态
     */
    @Override
    public void successPay(Long orderId) {

        //1. 修改订单的状态 → 已支付
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setOrderStatus(OrderStatus.PAID.name());
        orderInfo.setId(orderId);
        orderInfoMapper.updateById(orderInfo);
        /*LambdaUpdateWrapper<OrderInfo> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
        lambdaUpdateWrapper.eq(OrderInfo::getId, orderId)
                .set(OrderInfo::getOrderStatus, OrderStatus.PAID.name());
        orderInfoMapper.update(null, lambdaUpdateWrapper);*/


        // 2. 在订单服务中：远程调用库存服务的接口，扣减库存(锁定库存)
            //如果这笔订单中的商品不在同一个仓库中，那么仓储服务在扣减库存的时候，会调用订单服务的接口，进行拆单;
            //如果在同一个仓库中，跳过此步
        // 在库存服务中，如何知道这个订单有哪些商品呢？
            // 在库存服务中，扣减库存的时候，会远程调用订单服务的接口，查询订单信息以及订单明细，所以这就要求，在订单服务中，有一个接口，能够根据订单id查询订单信息和订单明细
        wareApiClient.decreaseStock(orderId);
    }

    /**
     * 7. 支付回调：库存扣减完成，修改订单状态为待发货/库存扣减异常,
     *      如果库存工作单的状态是DEDUCTED，则说明扣减库存成功于是将订单状态修改为待发货状态
     */
    @Override
    public void successLockStock(String orderId, String taskStatus) {

        if ("DEDUCTED".equals(taskStatus)){
            // 说明库存扣减成功
            // 修改订单状态为 [待发货]
            OrderInfo orderInfo = new OrderInfo();
            orderInfo.setOrderStatus(OrderStatus.WAIT_DELEVER.name());
            orderInfo.setId(Long.valueOf(orderId));

            orderInfoMapper.updateById(orderInfo);

        } else {
            // 说明仓库扣减库存失败了
            // 修改订单状态为 [库存扣减异常]
            OrderInfo orderInfo = new OrderInfo();
            orderInfo.setOrderStatus(OrderStatus.STOCK_EXCEPTION.name());
            orderInfo.setId(Long.valueOf(orderId));

            orderInfoMapper.updateById(orderInfo);
        }
    }

    /**
     * 8. 支付回调：拆单
     * @param orderId
     * @param wareSkuDTOList  表示在原来的订单中，原来订单中的各个商品分别都在哪些仓库中， 有多少个仓库，就需要拆分成多少个子订单
     * Demo：
     *      skuId    wareId
     *        29       1
     *        30       2
     *        31       1
     *        32       2
     *      List<WareSkuDTO>
     *           wareSkuDTO   wareId  → 1   skuIds:[29,31]
     *           wareSkuDTO   wareId  → 2   skuIds:[30,32]
     * @return
     */
    @Override
    public List<WareOrderTaskDTO> orderSplit(String orderId, List<WareSkuDTO> wareSkuDTOList) {

        //1. 创建拆单之后需要返回存储的List<WareOrderTaskDTO>
        ArrayList<WareOrderTaskDTO> wareOrderTaskDTOS = new ArrayList<>();

        //2. 根据orderId查询订单信息和订单详细信息
        OrderInfoDTO orderInfoDTO = getOrderInfo(Long.valueOf(orderId));          //订单信息，这里查询出来的订单状态为已支付
        List<OrderDetailDTO> orderDetailList = orderInfoDTO.getOrderDetailList(); //订单详细信息

        //3. 根据第二步查询结果和wareSkuDTOList创建每个仓库对应的子订单和子订单明细
        for (WareSkuDTO wareSkuDTO : wareSkuDTOList) {

            //3.1 获取仓库id和子订单的商品id对应的List
            String wareId = wareSkuDTO.getWareId();       //仓库id
            List<String> skuIds = wareSkuDTO.getSkuIds(); //子订单的商品id对应的List

            //3.2 创建子订单 ——> 根据原订单复制出子订单，然后在修改子订单中和原订单不一样的字段
            OrderInfo subOrderInfo = orderInfoConverter.copyOrderInfo(orderInfoDTO);
            //3.2.1 给子订单和父订单不一样的字段设值
            subOrderInfo.setId(null);                                  // 为了给id自增
            subOrderInfo.setParentOrderId(orderInfoDTO.getId());       // 父订单id
            subOrderInfo.setOutTradeNo(UUID.randomUUID().toString());  // 设置外部订单编号
            subOrderInfo.sumTotalAmount();                             // 计算总金额并设值
            subOrderInfo.setTradeBody(orderInfoDTO.getTradeBody());    // 设置为父订单的订单描述
            //3.2.2 给子订单设置子订单详细信息
            //设置一个存放子订单的商品详细信息的List
            ArrayList<OrderDetail> subOrderDetails = new ArrayList<>();
            //遍历查询出来的子订单
            for (OrderDetailDTO orderDetailDTO : orderDetailList) {
                Long skuId = orderDetailDTO.getSkuId();
                //判断：查询出来的原订单中商品详细信息对应skuId是否包含在某个仓库下面商品id列表中
                if (skuIds.contains(skuId.toString())){
                    //如果这个商品在这个某号仓库中，那么就添加到子订单中商品详细信息的列表中List<OrderDetail> orderDetailList
                    OrderDetail orderDetail = orderDetailConverter.convertOrderDetailToDTO(orderDetailDTO);
                    //给这个orderDetail添加到子订单中商品详细信息的列表中
                    //orderDetail.setId(null); // 防止后续保存子订单明细的时候，id重复，这个地方我写在了保存订单信息的时候，所以这里就不需要了
                    subOrderDetails.add(orderDetail);
                }
            }
            //把子订单商品详细列表放入子订单的订单信息中
            subOrderInfo.setOrderDetailList(subOrderDetails);

            //3.3 保存子订单和子订单明细到数据库中
            this.saveOrderInfo(subOrderInfo);

            //3.4 把子订单转化为库存工作表
            WareOrderTaskDTO wareOrderTaskDTO = orderInfoConverter.convertOrderInfoToWareOrderTaskDTO(subOrderInfo);
            wareOrderTaskDTO.setWareId(wareId); //给库存工作表设置仓库id
            wareOrderTaskDTOS.add(wareOrderTaskDTO); //把库存工作表放到第一步定义的每一个仓库对应的子库存工作表的List中
        }

        //4. 修改原来的订单状态为已拆分
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setId(Long.valueOf(orderId));
        orderInfo.setOrderStatus(OrderStatus.SPLIT.name());
        orderInfoMapper.updateById(orderInfo);

        //5. 返回拆单结果
        return wareOrderTaskDTOS;
    }

    /**
     * 9. 订单超时自动取消的业务逻辑
     * @param orderId
     */
    @Override
    public void execExpiredOrder(Long orderId) {
        /*
        //1. 根据订单id查询订单信息
        OrderInfoDTO orderInfoDTO = this.getOrderInfo(orderId);

        //2. 获取订单状态，对订单状态进行判断：如果订单状态是未支付，那么修改为已关闭
        if (orderInfoDTO.getOrderStatus().equals(OrderStatus.UNPAID.name())){
            orderInfoDTO.setOrderStatus(OrderStatus.CLOSED.name());

            //将orderInfoDTO转换为PO对象orderInfo
            OrderInfo orderInfo = orderInfoConverter.copyOrderInfo(orderInfoDTO);
            orderInfoMapper.updateById(orderInfo);
        }
        */


        //上面是之前的逻辑，我们现在需要考虑临界情况
            //比如：用户在12:00:00的时候下单了，现在系统设置的订单超时自动取消的时间是30min，那么就意味着这个订单的延迟消息会在12:30:00被消费，那么就意味着给支付宝传递的支付超时时间也是12:30:00
            // 假如，用户在12:29:59的时候，去支付，可以成功，支付成功了之后，支付宝要进行异步回调,正常情况下是支付宝立即进行异步回调
            // 但是网络是存在延迟与波动的, 所以我们的系统收到支付回调的时间 可能在12:30:00 之后，比如在12:30:05 才收到支付回调，而订单超时自动取消的消息，会在 12:30:00 的时候进行消费
            // 消费的逻辑是 判断订单状态是否是未支付，如果是未支付，那么修改订单状态为【已关闭】
            // 那么等待12:30:05秒进行支付回调的时候，发现这个订单已经关闭了,那么就存在问题
            // 所以，在进行订单的延迟消息消费的时候，不能够简单的通过订单状态是未支付，来判断用户是否支付了

        // 为了解决以上的问题，完善的业务逻辑:
        // 1. 根据订单id查询订单信息，如果订单状态不是未支付， 啥也不做
        OrderInfoDTO orderInfoDTO = this.getOrderInfo(orderId);

        // 2. 如果订单状态是未支付
        if (orderInfoDTO.getOrderStatus().equals(OrderStatus.UNPAID.name())){
            // a) 远程调用支付服务的接口，查询该订单是否有对应的支付记录
            PaymentInfoDTO paymentInfoDTO = payApiClient.getPaymentInfoDTOByOutTradeNo(orderInfoDTO.getOutTradeNo());

            //2.1 如果没有对应的支付记录, 说明用户没有点击【扫码支付】，关闭订单
            if (paymentInfoDTO == null){
                closeOrder(orderInfoDTO);
            }
            //2.2 如果支付服务的表中有对应的支付记录, 说明用户点击了【扫码支付】, 查询支付宝支付状态
            else {
                // b) 根据out_trade_no查询支付宝的支付状态
                Result<Map<String, String>> alipayInfoMap = payApiClient.getAlipayInfo(orderInfoDTO.getOutTradeNo());
                String subCode = (alipayInfoMap.getData()).get("subCode");
                String tradeStatus = (alipayInfoMap.getData()).get("tradeStatus");

                //2.2.1 如果支付宝查询到的结果是 【subCode = ACQ.TRADE_NOT_EXIST】 说明支付宝不存在支付记录, 说明用户没有输入账号和密码  →   关闭订单、关闭本地支付表中的记录
                if ("ACQ.TRADE_NOT_EXIST".equals(subCode)){
                    closeOrder(orderInfoDTO);
                    payApiClient.closePaymentInfo(orderInfoDTO.getOutTradeNo());
                }

                // 如果支付宝查询到的结果是 【subCode = null &&  tradeStatus = WAIT_BUYER_PAY】，说明用户输入了账号和密码，但是没有输入支付密码 →   关闭订单、关闭本地支付表，关闭支付宝交易接口
                if (subCode == null && "WAIT_BUYER_PAY".equals(tradeStatus)){
                    closeOrder(orderInfoDTO);
                    payApiClient.closePaymentInfo(orderInfoDTO.getOutTradeNo());
                    payApiClient.closeAlipay(orderInfoDTO.getOutTradeNo());
                }

                // 如果支付宝查询到的结果是 【subCode = null && tradeStatus = TRADE_CLOSED】, 说明支付宝那边已经把交易关闭了 →  关闭订单, 关闭本地支付表
                if (subCode == null && "TRADE_CLOSED".equals(tradeStatus)){
                    closeOrder(orderInfoDTO);
                    payApiClient.closePaymentInfo(orderInfoDTO.getOutTradeNo());
                }


                //2.2.2 如果支付宝查询到的结果是 【subCode = null && tradeStatus = TRADE_SUCCESS】， 说明用户已经支付了，只是异步回调还没有回调回来 →  啥也不做
                //如果支付服务正常支付，订单服务这边什么操作都不做，因为异步回调等会就会过来，修改订单的操作由异步回调工作来做
            }
        }
    }


    /**
     * 10. 保存秒杀订单以及订单详情，返回orderId
     * @param orderInfoParam
     * @return
     */
    @Override
    @Transactional(rollbackFor = RuntimeException.class)
    public Long saveSeckillOrder(OrderInfoParam orderInfoParam) {

        //1. 转换
        OrderInfo orderInfo = orderInfoConverter.convertOrderInfoParam(orderInfoParam);

        //2. 将没有值的字段再单独设值
        orderInfo.setOrderStatus(OrderStatus.UNPAID.name()); //生成订单状态
        orderInfo.sumTotalAmount();                          //设置订单中商品总价格
        orderInfo.setOutTradeNo(UUID.randomUUID().toString().replace("-", "")); //设置外部订单编号
        orderInfo.setTradeBody(orderInfoParam.getOrderDetailList().get(0).getSkuName());         //设置商品订单标题
        orderInfo.setOrderType(OrderType.PROMO_ORDER.name());                                    //设置订单类型
        orderInfo.setExpireTime(DateUtil.datePlusMinutes(new Date(), 1));                //设置订单过期时间

        //3. 保存订单和订单明细
        /*
            对于这种本地方法调用，如果需要使用事务来控制，应该在当前方法上增加@Transactional注解，这个本质是AOP
            调用这个方法的对象，必须是容器中管理的对象，只有是容器中管理的对象，才会被aop增强
            如果是new出来的对象和this调用本地方法，都是无法被aop增强的
         */

        Long orderId = this.saveOrderInfo(orderInfo);

        //4. todo 发送订单超时自动取消的消息
            //和普通订单没有太多的区别，只需要注意，秒杀订单，在自动取消的时候，需要回补库存

        //5. 返回保存的订单id
        return orderId;
    }

    /**
     * 11. 自定义关闭订单 → 修改订单状态为已关闭
     * @param orderInfoDTO
     */
    private void closeOrder(OrderInfoDTO orderInfoDTO) {

        orderInfoDTO.setOrderStatus(OrderStatus.CLOSED.name());

        //将orderInfoDTO转换为PO对象orderInfo
        OrderInfo orderInfo = orderInfoConverter.copyOrderInfo(orderInfoDTO);
        orderInfoMapper.updateById(orderInfo);
    }
}
